// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package rpcchainvm

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"reflect"
	"slices"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc"
	"google.golang.org/grpc/test/bufconn"

	"github.com/ava-labs/avalanchego/api/metrics"
	"github.com/ava-labs/avalanchego/snow/engine/enginetest"
	"github.com/ava-labs/avalanchego/snow/engine/snowman/block"
	"github.com/ava-labs/avalanchego/snow/engine/snowman/block/blockmock"
	"github.com/ava-labs/avalanchego/snow/engine/snowman/block/blocktest"
	"github.com/ava-labs/avalanchego/upgrade"
	"github.com/ava-labs/avalanchego/utils"
	"github.com/ava-labs/avalanchego/utils/constants"
	"github.com/ava-labs/avalanchego/utils/logging"
	"github.com/ava-labs/avalanchego/vms/rpcchainvm/grpcutils"
	"github.com/ava-labs/avalanchego/vms/rpcchainvm/runtime"
	"github.com/ava-labs/avalanchego/vms/rpcchainvm/runtime/subprocess"

	vmpb "github.com/ava-labs/avalanchego/proto/pb/vm"
)

const (
	chainVMTestKey                                 = "chainVMTest"
	stateSyncEnabledTestKey                        = "stateSyncEnabledTest"
	getOngoingSyncStateSummaryTestKey              = "getOngoingSyncStateSummaryTest"
	getLastStateSummaryTestKey                     = "getLastStateSummaryTest"
	parseStateSummaryTestKey                       = "parseStateSummaryTest"
	getStateSummaryTestKey                         = "getStateSummaryTest"
	acceptStateSummaryTestKey                      = "acceptStateSummaryTest"
	lastAcceptedBlockPostStateSummaryAcceptTestKey = "lastAcceptedBlockPostStateSummaryAcceptTest"
	contextTestKey                                 = "contextTest"
	batchedParseBlockCachingTestKey                = "batchedParseBlockCachingTest"
)

var TestServerPluginMap = map[string]func(*testing.T, bool) block.ChainVM{
	stateSyncEnabledTestKey:                        stateSyncEnabledTestPlugin,
	getOngoingSyncStateSummaryTestKey:              getOngoingSyncStateSummaryTestPlugin,
	getLastStateSummaryTestKey:                     getLastStateSummaryTestPlugin,
	parseStateSummaryTestKey:                       parseStateSummaryTestPlugin,
	getStateSummaryTestKey:                         getStateSummaryTestPlugin,
	acceptStateSummaryTestKey:                      acceptStateSummaryTestPlugin,
	lastAcceptedBlockPostStateSummaryAcceptTestKey: lastAcceptedBlockPostStateSummaryAcceptTestPlugin,
	contextTestKey:                                 contextEnabledTestPlugin,
	batchedParseBlockCachingTestKey:                batchedParseBlockCachingTestPlugin,
}

// helperProcess helps with creating the subnet binary for testing.
func helperProcess(s ...string) *exec.Cmd {
	cs := []string{"-test.run=TestHelperProcess", "--"}
	cs = append(cs, s...)
	env := []string{
		"TEST_PROCESS=1",
	}
	run := os.Args[0]
	cmd := exec.Command(run, cs...)
	env = append(env, os.Environ()...)
	cmd.Env = env
	return cmd
}

func TestHelperProcess(t *testing.T) {
	if os.Getenv("TEST_PROCESS") != "1" {
		return
	}

	args := os.Args
	for len(args) > 0 {
		if args[0] == "--" {
			args = args[1:]
			break
		}
		args = args[1:]
	}

	if len(args) == 0 {
		fmt.Fprintln(os.Stderr, "failed to receive testKey")
		os.Exit(2)
	}

	testKey := args[0]
	if testKey == "dummy" {
		// block till killed
		select {}
	}

	mockedVM := TestServerPluginMap[testKey](t, true /*loadExpectations*/)
	err := Serve(t.Context(), mockedVM)
	if err != nil {
		os.Exit(1)
	}

	os.Exit(0)
}

// TestVMServerInterface ensures that the RPCs methods defined by VMServer
// interface are implemented.
func TestVMServerInterface(t *testing.T) {
	var wantMethods, gotMethods []string
	pb := reflect.TypeFor[vmpb.VMServer]()
	for i := 0; i < pb.NumMethod()-1; i++ {
		wantMethods = append(wantMethods, pb.Method(i).Name)
	}
	slices.Sort(wantMethods)

	impl := reflect.TypeFor[*VMServer]()
	for i := 0; i < impl.NumMethod(); i++ {
		gotMethods = append(gotMethods, impl.Method(i).Name)
	}
	slices.Sort(gotMethods)

	require.Equal(t, wantMethods, gotMethods)
}

func TestRuntimeSubprocessBootstrap(t *testing.T) {
	tests := []struct {
		name      string
		config    *subprocess.Config
		assertErr func(require *require.Assertions, err error)
		// if false vm initialize bootstrap will fail
		serveVM bool
	}{
		{
			name: "happy path",
			config: &subprocess.Config{
				Stderr:           logging.NoLog{},
				Stdout:           logging.NoLog{},
				Log:              logging.NoLog{},
				HandshakeTimeout: runtime.DefaultHandshakeTimeout,
			},
			assertErr: func(require *require.Assertions, err error) {
				require.NoError(err)
			},
			serveVM: true,
		},
		{
			name: "invalid stderr",
			config: &subprocess.Config{
				Stdout:           logging.NoLog{},
				Log:              logging.NoLog{},
				HandshakeTimeout: runtime.DefaultHandshakeTimeout,
			},
			assertErr: func(require *require.Assertions, err error) {
				require.ErrorIs(err, runtime.ErrInvalidConfig)
			},
			serveVM: true,
		},
		{
			name: "handshake timeout",
			config: &subprocess.Config{
				Stderr:           logging.NoLog{},
				Stdout:           logging.NoLog{},
				Log:              logging.NoLog{},
				HandshakeTimeout: time.Microsecond,
			},
			assertErr: func(require *require.Assertions, err error) {
				require.ErrorIs(err, runtime.ErrHandshakeFailed)
			},
			serveVM: false,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			require := require.New(t)

			ctrl := gomock.NewController(t)
			vm := blockmock.NewChainVM(ctrl)

			listener, err := grpcutils.NewListener()
			require.NoError(err)

			t.Setenv(runtime.EngineAddressKey, listener.Addr().String())

			ctx := t.Context()
			if test.serveVM {
				go func() {
					_ = Serve(ctx, vm)
				}()
			}

			status, stopper, err := subprocess.Bootstrap(
				t.Context(),
				listener,
				helperProcess("dummy"),
				test.config,
			)
			if err == nil {
				require.NotEmpty(status.Addr)
				stopper.Stop(ctx)
			}
			test.assertErr(require, err)
		})
	}
}

func TestNewHTTPHandler(t *testing.T) {
	require := require.New(t)

	grpcServer := grpc.NewServer()
	listener := bufconn.Listen(1024)

	serverVM := &blocktest.VM{
		VM: enginetest.VM{
			NewHTTPHandlerF: func(context.Context) (http.Handler, error) {
				return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
					w.WriteHeader(http.StatusOK)
				}), nil
			},
		},
	}

	server := NewServer(serverVM, utils.NewAtomic[bool](false))
	vmpb.RegisterVMServer(grpcServer, server)

	go func() {
		_ = grpcServer.Serve(listener)
	}()

	cc, err := grpc.DialContext(t.Context(), "bufnet",
		grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
			return listener.Dial()
		}),
		grpc.WithInsecure(),
	)
	require.NoError(err)

	client := NewClient(
		cc,
		runtime.NewManager(),
		123,
		nil,
		metrics.NewLabelGatherer(""),
		logging.NoLog{},
	)

	handler, err := client.NewHTTPHandler(t.Context())
	require.NoError(err)

	w := httptest.NewRecorder()
	handler.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/", nil))
	require.Equal(http.StatusOK, w.Code)
}

// TestConvertNetworkUpgrades_AllFieldsHandled ensures that all fields in
// upgrade.Config are properly handled when converting from the gRPC protobuf message.
func TestConvertNetworkUpgrades_AllFieldsHandled(t *testing.T) {
	type networks struct {
		name      string
		networkID uint32
	}

	tests := []networks{
		{
			name:      "Mainnet",
			networkID: constants.MainnetID,
		},
		{
			name:      "Fuji",
			networkID: constants.FujiID,
		},
		{
			name:      "Local",
			networkID: constants.LocalID,
		},
		{
			name:      "UnitTest",
			networkID: constants.UnitTestID,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			wantConfig := upgrade.GetConfig(test.networkID)
			networkUpgrades := getNetworkUpgrades(wantConfig)
			gotConfig, err := convertNetworkUpgrades(networkUpgrades)
			require.NoError(t, err)
			require.Equal(t, wantConfig, gotConfig, "convertNetworkUpgrades did not return expected config. Update grpcutils! (convertNetworkUpgrades and vm_client.go)")
		})
	}
}
